Skip to main content
Glama

Edit-MCP

mod.rs93.5 kB
// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. //! A text buffer for a text editor. //! //! Implements a Unicode-aware, layout-aware text buffer for terminals. //! It's based on a gap buffer. It has no line cache and instead relies //! on the performance of the ucd module for fast text navigation. //! //! --- //! //! If the project ever outgrows a basic gap buffer (e.g. to add time travel) //! an ideal, alternative architecture would be a piece table with immutable trees. //! The tree nodes can be allocated on the same arena allocator as the added chunks, //! making lifetime management fairly easy. The algorithm is described here: //! * <https://cdacamar.github.io/data%20structures/algorithms/benchmarking/text%20editors/c++/editor-data-structures/> //! * <https://github.com/cdacamar/fredbuf> //! //! The downside is that text navigation & search takes a performance hit due to small chunks. //! The solution to the former is to keep line caches, which further complicates the architecture. //! There's no solution for the latter. However, there's a chance that the performance will still be sufficient. mod gap_buffer; mod navigation; use std::borrow::Cow; use std::cell::UnsafeCell; use std::collections::LinkedList; use std::fmt::Write as _; use std::fs::File; use std::io::{Read as _, Write as _}; use std::mem::{self, MaybeUninit}; use std::ops::Range; use std::rc::Rc; use std::str; pub use gap_buffer::GapBuffer; use crate::arena::{ArenaString, scratch_arena}; use crate::cell::SemiRefCell; use crate::document::{ReadableDocument, WriteableDocument}; use crate::framebuffer::{Framebuffer, IndexedColor}; use crate::helpers::*; use crate::oklab::oklab_blend; use crate::simd::memchr2; use crate::unicode::{self, Cursor, MeasurementConfig}; use crate::{apperr, icu, simd}; /// The margin template is used for line numbers. /// The max. line number we should ever expect is probably 64-bit, /// and so this template fits 19 digits, followed by " │ ". const MARGIN_TEMPLATE: &str = " │ "; /// Just a bunch of whitespace you can use for turning tabs into spaces. /// Happens to reuse MARGIN_TEMPLATE, because it has sufficient whitespace. const TAB_WHITESPACE: &str = MARGIN_TEMPLATE; /// Stores statistics about the whole document. #[derive(Copy, Clone)] pub struct TextBufferStatistics { logical_lines: CoordType, visual_lines: CoordType, } /// Stores the active text selection anchors. /// /// The two points are not sorted. Instead, `beg` refers to where the selection /// started being made and `end` refers to the currently being updated position. #[derive(Copy, Clone)] struct TextBufferSelection { beg: Point, end: Point, } /// In order to group actions into a single undo step, /// we need to know the type of action that was performed. /// This stores the action type. #[derive(Copy, Clone, Eq, PartialEq)] enum HistoryType { Other, Write, Delete, } /// An undo/redo entry. struct HistoryEntry { /// [`TextBuffer::cursor`] position before the change was made. cursor_before: Point, /// [`TextBuffer::selection`] before the change was made. selection_before: Option<TextBufferSelection>, /// [`TextBuffer::stats`] before the change was made. stats_before: TextBufferStatistics, /// [`GapBuffer::generation`] before the change was made. generation_before: u32, /// Logical cursor position where the change took place. /// The position is at the start of the changed range. cursor: Point, /// Text that was deleted from the buffer. deleted: Vec<u8>, /// Text that was added to the buffer. added: Vec<u8>, } /// Caches an ICU search operation. struct ActiveSearch { /// The search pattern. pattern: String, /// The search options. options: SearchOptions, /// The ICU `UText` object. text: icu::Text, /// The ICU `URegularExpression` object. regex: icu::Regex, /// [`GapBuffer::generation`] when the search was created. /// This is used to detect if we need to refresh the /// [`ActiveSearch::regex`] object. buffer_generation: u32, /// [`TextBuffer::selection_generation`] when the search was /// created. When the user manually selects text, we need to /// refresh the [`ActiveSearch::pattern`] with it. selection_generation: u32, /// Stores the text buffer offset in between searches. next_search_offset: usize, /// If we know there were no hits, we can skip searching. no_matches: bool, } /// Options for a search operation. #[derive(Default, Clone, Copy, Eq, PartialEq)] pub struct SearchOptions { /// If true, the search is case-sensitive. pub match_case: bool, /// If true, the search matches whole words. pub whole_word: bool, /// If true, the search uses regex. pub use_regex: bool, } /// Caches the start and length of the active edit line for a single edit. /// This helps us avoid having to remeasure the buffer after an edit. struct ActiveEditLineInfo { /// Points to the start of the currently being edited line. safe_start: Cursor, /// Number of visual rows of the line that starts /// at [`ActiveEditLineInfo::safe_start`]. line_height_in_rows: CoordType, /// Byte distance from the start of the line at /// [`ActiveEditLineInfo::safe_start`] to the next line. distance_next_line_start: usize, } /// Char- or word-wise navigation? Your choice. pub enum CursorMovement { Grapheme, Word, } /// The result of a call to [`TextBuffer::render()`]. pub struct RenderResult { /// The maximum visual X position we encountered during rendering. pub visual_pos_x_max: CoordType, } /// A [`TextBuffer`] with inner mutability. pub type TextBufferCell = SemiRefCell<TextBuffer>; /// A [`TextBuffer`] inside an [`Rc`]. /// /// We need this because the TUI system needs to borrow /// the given text buffer(s) until after the layout process. pub type RcTextBuffer = Rc<TextBufferCell>; /// A text buffer for a text editor. pub struct TextBuffer { buffer: GapBuffer, undo_stack: LinkedList<SemiRefCell<HistoryEntry>>, redo_stack: LinkedList<SemiRefCell<HistoryEntry>>, last_history_type: HistoryType, last_save_generation: u32, active_edit_line_info: Option<ActiveEditLineInfo>, active_edit_depth: i32, active_edit_off: usize, stats: TextBufferStatistics, cursor: Cursor, // When scrolling significant amounts of text away from the cursor, // rendering will naturally slow down proportionally to the distance. // To avoid this, we cache the cursor position for rendering. // Must be cleared on every edit or reflow. cursor_for_rendering: Option<Cursor>, selection: Option<TextBufferSelection>, selection_generation: u32, search: Option<UnsafeCell<ActiveSearch>>, width: CoordType, margin_width: CoordType, margin_enabled: bool, word_wrap_column: CoordType, word_wrap_enabled: bool, tab_size: CoordType, indent_with_tabs: bool, line_highlight_enabled: bool, ruler: CoordType, encoding: &'static str, newlines_are_crlf: bool, insert_final_newline: bool, overtype: bool, wants_cursor_visibility: bool, } impl TextBuffer { /// Creates a new text buffer inside an [`Rc`]. /// See [`TextBuffer::new()`]. pub fn new_rc(small: bool) -> apperr::Result<RcTextBuffer> { let buffer = Self::new(small)?; Ok(Rc::new(SemiRefCell::new(buffer))) } /// Creates a new text buffer. With `small` you can control /// if the buffer is optimized for <1MiB contents. pub fn new(small: bool) -> apperr::Result<Self> { Ok(Self { buffer: GapBuffer::new(small)?, undo_stack: LinkedList::new(), redo_stack: LinkedList::new(), last_history_type: HistoryType::Other, last_save_generation: 0, active_edit_line_info: None, active_edit_depth: 0, active_edit_off: 0, stats: TextBufferStatistics { logical_lines: 1, visual_lines: 1 }, cursor: Default::default(), cursor_for_rendering: None, selection: None, selection_generation: 0, search: None, width: 0, margin_width: 0, margin_enabled: false, word_wrap_column: 0, word_wrap_enabled: false, tab_size: 4, indent_with_tabs: false, line_highlight_enabled: false, ruler: 0, encoding: "UTF-8", newlines_are_crlf: cfg!(windows), // Windows users want CRLF insert_final_newline: false, overtype: false, wants_cursor_visibility: false, }) } /// Length of the document in bytes. pub fn text_length(&self) -> usize { self.buffer.len() } /// Number of logical lines in the document, /// that is, lines separated by newlines. pub fn logical_line_count(&self) -> CoordType { self.stats.logical_lines } /// Number of visual lines in the document, /// that is, the number of lines after layout. pub fn visual_line_count(&self) -> CoordType { self.stats.visual_lines } /// Does the buffer need to be saved? pub fn is_dirty(&self) -> bool { self.last_save_generation != self.buffer.generation() } /// The buffer generation changes on every edit. /// With this you can check if it has changed since /// the last time you called this function. pub fn generation(&self) -> u32 { self.buffer.generation() } /// Force the buffer to be dirty. pub fn mark_as_dirty(&mut self) { self.last_save_generation = self.buffer.generation().wrapping_sub(1); } fn mark_as_clean(&mut self) { self.last_save_generation = self.buffer.generation(); } /// The encoding used during reading/writing. "UTF-8" is the default. pub fn encoding(&self) -> &'static str { self.encoding } /// Set the encoding used during reading/writing. pub fn set_encoding(&mut self, encoding: &'static str) { if self.encoding != encoding { self.encoding = encoding; self.mark_as_dirty(); } } /// The newline type used in the document. LF or CRLF. pub fn is_crlf(&self) -> bool { self.newlines_are_crlf } /// Changes the newline type without normalizing the document. pub fn set_crlf(&mut self, crlf: bool) { self.newlines_are_crlf = crlf; } /// Changes the newline type used in the document. /// /// NOTE: Cannot be undone. pub fn normalize_newlines(&mut self, crlf: bool) { let newline: &[u8] = if crlf { b"\r\n" } else { b"\n" }; let mut off = 0; let mut cursor_offset = self.cursor.offset; let mut cursor_for_rendering_offset = self.cursor_for_rendering.map_or(cursor_offset, |c| c.offset); #[cfg(debug_assertions)] let mut adjusted_newlines = 0; 'outer: loop { // Seek to the offset of the next line start. loop { let chunk = self.read_forward(off); if chunk.is_empty() { break 'outer; } let (delta, line) = simd::lines_fwd(chunk, 0, 0, 1); off += delta; if line == 1 { break; } } // Get the preceding newline. let chunk = self.read_backward(off); let chunk_newline_len = if chunk.ends_with(b"\r\n") { 2 } else { 1 }; let chunk_newline = &chunk[chunk.len() - chunk_newline_len..]; if chunk_newline != newline { // If this newline is still before our cursor position, then it still has an effect on its offset. // Any newline adjustments past that cursor position are irrelevant. let delta = newline.len() as isize - chunk_newline_len as isize; if off <= cursor_offset { cursor_offset = cursor_offset.saturating_add_signed(delta); #[cfg(debug_assertions)] { adjusted_newlines += 1; } } if off <= cursor_for_rendering_offset { cursor_for_rendering_offset = cursor_for_rendering_offset.saturating_add_signed(delta); } // Replace the newline. off -= chunk_newline_len; self.buffer.replace(off..off + chunk_newline_len, newline); off += newline.len(); } } // If this fails, the cursor offset calculation above is wrong. #[cfg(debug_assertions)] debug_assert_eq!(adjusted_newlines, self.cursor.logical_pos.y); self.cursor.offset = cursor_offset; if let Some(cursor) = &mut self.cursor_for_rendering { cursor.offset = cursor_for_rendering_offset; } self.newlines_are_crlf = crlf; } /// If enabled, automatically insert a final newline /// when typing at the end of the file. pub fn set_insert_final_newline(&mut self, enabled: bool) { self.insert_final_newline = enabled; } /// Whether to insert or overtype text when writing. pub fn is_overtype(&self) -> bool { self.overtype } /// Set the overtype mode. pub fn set_overtype(&mut self, overtype: bool) { self.overtype = overtype; } /// Gets the logical cursor position, that is, /// the position in lines and graphemes per line. pub fn cursor_logical_pos(&self) -> Point { self.cursor.logical_pos } /// Gets the visual cursor position, that is, /// the position in laid out rows and columns. pub fn cursor_visual_pos(&self) -> Point { self.cursor.visual_pos } /// Gets the width of the left margin. pub fn margin_width(&self) -> CoordType { self.margin_width } /// Is the left margin enabled? pub fn set_margin_enabled(&mut self, enabled: bool) -> bool { if self.margin_enabled == enabled { false } else { self.margin_enabled = enabled; self.reflow(); true } } /// Gets the width of the text contents for layout. pub fn text_width(&self) -> CoordType { self.width - self.margin_width } /// Ask the TUI system to scroll the buffer and make the cursor visible. /// /// TODO: This function shows that [`TextBuffer`] is poorly abstracted /// away from the TUI system. The only reason this exists is so that /// if someone outside the TUI code enables word-wrap, the TUI code /// recognizes this and scrolls the cursor into view. But outside of this /// scrolling, views, etc., are all UI concerns = this should not be here. pub fn make_cursor_visible(&mut self) { self.wants_cursor_visibility = true; } /// For the TUI code to retrieve a prior [`TextBuffer::make_cursor_visible()`] request. pub fn take_cursor_visibility_request(&mut self) -> bool { mem::take(&mut self.wants_cursor_visibility) } /// Is word-wrap enabled? /// /// Technically, this is a misnomer, because it's line-wrapping. pub fn is_word_wrap_enabled(&self) -> bool { self.word_wrap_enabled } /// Enable or disable word-wrap. /// /// NOTE: It's expected that the tui code calls `set_width()` sometime after this. /// This will then trigger the actual recalculation of the cursor position. pub fn set_word_wrap(&mut self, enabled: bool) { if self.word_wrap_enabled != enabled { self.word_wrap_enabled = enabled; self.width = 0; // Force a reflow. self.make_cursor_visible(); } } /// Set the width available for layout. /// /// Ideally this would be a pure UI concern, but the text buffer needs this /// so that it can abstract away visual cursor movement such as "go a line up". /// What would that even mean if it didn't know how wide a line is? pub fn set_width(&mut self, width: CoordType) -> bool { if width <= 0 || width == self.width { false } else { self.width = width; self.reflow(); true } } /// Set the tab width. Could be anything, but is expected to be 1-8. pub fn tab_size(&self) -> CoordType { self.tab_size } /// Set the tab size. Clamped to 1-8. pub fn set_tab_size(&mut self, width: CoordType) -> bool { let width = width.clamp(1, 8); if width == self.tab_size { false } else { self.tab_size = width; self.reflow(); true } } /// Returns whether tabs are used for indentation. pub fn indent_with_tabs(&self) -> bool { self.indent_with_tabs } /// Sets whether tabs or spaces are used for indentation. pub fn set_indent_with_tabs(&mut self, indent_with_tabs: bool) { self.indent_with_tabs = indent_with_tabs; } /// Sets whether the line the cursor is on should be highlighted. pub fn set_line_highlight_enabled(&mut self, enabled: bool) { self.line_highlight_enabled = enabled; } /// Sets a ruler column, e.g. 80. pub fn set_ruler(&mut self, column: CoordType) { self.ruler = column; } pub fn reflow(&mut self) { // +1 onto logical_lines, because line numbers are 1-based. // +1 onto log10, because we want the digit width and not the actual log10. // +3 onto log10, because we append " | " to the line numbers to form the margin. self.margin_width = if self.margin_enabled { self.stats.logical_lines.ilog10() as CoordType + 4 } else { 0 }; let text_width = self.text_width(); // 2 columns are required, because otherwise wide glyphs wouldn't ever fit. self.word_wrap_column = if self.word_wrap_enabled && text_width >= 2 { text_width } else { 0 }; // Recalculate the cursor position. self.cursor = self.cursor_move_to_logical_internal( if self.word_wrap_column > 0 { Default::default() } else { self.goto_line_start(self.cursor, self.cursor.logical_pos.y) }, self.cursor.logical_pos, ); // Recalculate the line statistics. if self.word_wrap_column > 0 { let end = self.cursor_move_to_logical_internal(self.cursor, Point::MAX); self.stats.visual_lines = end.visual_pos.y + 1; } else { self.stats.visual_lines = self.stats.logical_lines; } self.cursor_for_rendering = None; } /// Replaces the entire buffer contents with the given `text`. /// Assumes that the line count doesn't change. pub fn copy_from_str(&mut self, text: &dyn ReadableDocument) { if self.buffer.copy_from(text) { self.recalc_after_content_swap(); self.cursor_move_to_logical(Point { x: CoordType::MAX, y: 0 }); let delete = self.buffer.len() - self.cursor.offset; if delete != 0 { self.buffer.allocate_gap(self.cursor.offset, 0, delete); } } } fn recalc_after_content_swap(&mut self) { // If the buffer was changed, nothing we previously saved can be relied upon. self.undo_stack.clear(); self.redo_stack.clear(); self.last_history_type = HistoryType::Other; self.cursor = Default::default(); self.cursor_for_rendering = None; self.set_selection(None); self.search = None; self.mark_as_clean(); self.reflow(); } /// Copies the contents of the buffer into a string. pub fn save_as_string(&mut self, dst: &mut dyn WriteableDocument) { self.buffer.copy_into(dst); self.mark_as_clean(); } /// Reads a file from disk into the text buffer, detecting encoding and BOM. pub fn read_file( &mut self, file: &mut File, encoding: Option<&'static str>, ) -> apperr::Result<()> { let scratch = scratch_arena(None); let mut buf = scratch.alloc_uninit().transpose(); let mut first_chunk_len = 0; let mut read = 0; // Read enough bytes to detect the BOM. while first_chunk_len < BOM_MAX_LEN { read = file_read_uninit(file, &mut buf[first_chunk_len..])?; if read == 0 { break; } first_chunk_len += read; } if let Some(encoding) = encoding { self.encoding = encoding; } else { let bom = detect_bom(unsafe { buf[..first_chunk_len].assume_init_ref() }); self.encoding = bom.unwrap_or("UTF-8"); } // TODO: Since reading the file can fail, we should ensure that we also reset the cursor here. // I don't do it, so that `recalc_after_content_swap()` works. self.buffer.clear(); let done = read == 0; if self.encoding == "UTF-8" { self.read_file_as_utf8(file, &mut buf, first_chunk_len, done)?; } else { self.read_file_with_icu(file, &mut buf, first_chunk_len, done)?; } // Figure out // * the logical line count // * the newline type (LF or CRLF) // * the indentation type (tabs or spaces) // * whether there's a final newline { let chunk = self.read_forward(0); let mut offset = 0; let mut lines = 0; // Number of lines ending in CRLF. let mut crlf_count = 0; // Number of lines starting with a tab. let mut tab_indentations = 0; // Number of lines starting with a space. let mut space_indentations = 0; // Histogram of the indentation depth of lines starting with between 2 and 8 spaces. // In other words, `space_indentation_sizes[0]` is the number of lines starting with 2 spaces. let mut space_indentation_sizes = [0; 7]; loop { // Check if the line starts with a tab. if offset < chunk.len() && chunk[offset] == b'\t' { tab_indentations += 1; } else { // Otherwise, check how many spaces the line starts with. Searching for >8 spaces // allows us to reject lines that have more than 1 level of indentation. let space_indentation = chunk[offset..].iter().take(9).take_while(|&&c| c == b' ').count(); // We'll also reject lines starting with 1 space, because that's too fickle as a heuristic. if (2..=8).contains(&space_indentation) { space_indentations += 1; // If we encounter an indentation depth of 6, it may either be a 6-space indentation, // two 3-space indentation or 3 2-space indentations. To make this work, we increment // all 3 possible histogram slots. // 2 -> 2 // 3 -> 3 // 4 -> 4 2 // 5 -> 5 // 6 -> 6 3 2 // 7 -> 7 // 8 -> 8 4 2 space_indentation_sizes[space_indentation - 2] += 1; if space_indentation & 4 != 0 { space_indentation_sizes[0] += 1; } if space_indentation == 6 || space_indentation == 8 { space_indentation_sizes[space_indentation / 2 - 2] += 1; } } } (offset, lines) = simd::lines_fwd(chunk, offset, lines, lines + 1); // Check if the preceding line ended in CRLF. if offset >= 2 && &chunk[offset - 2..offset] == b"\r\n" { crlf_count += 1; } // We'll limit our heuristics to the first 1000 lines. // That should hopefully be enough in practice. if offset >= chunk.len() || lines >= 1000 { break; } } // We'll assume CRLF if more than half of the lines end in CRLF. let newlines_are_crlf = crlf_count >= lines / 2; // We'll assume tabs if there are more lines starting with tabs than with spaces. let indent_with_tabs = tab_indentations > space_indentations; let tab_size = if indent_with_tabs { // Tabs will get a visual size of 4 spaces by default. 4 } else { // Otherwise, we'll assume the most common indentation depth. // If there are conflicting indentation depths, we'll prefer the maximum, because in the loop // above we incremented the histogram slot for 2-spaces when encountering 4-spaces and so on. let mut max = 1; let mut tab_size = 4; for (i, &count) in space_indentation_sizes.iter().enumerate() { if count >= max { max = count; tab_size = i as CoordType + 2; } } tab_size }; // If the file has more than 1000 lines, figure out how many are remaining. if offset < chunk.len() { (_, lines) = simd::lines_fwd(chunk, offset, lines, CoordType::MAX); } let final_newline = chunk.ends_with(b"\n"); // Add 1, because the last line doesn't end in a newline (it ends in the literal end). self.stats.logical_lines = lines + 1; self.stats.visual_lines = self.stats.logical_lines; self.newlines_are_crlf = newlines_are_crlf; self.insert_final_newline = final_newline; self.indent_with_tabs = indent_with_tabs; self.tab_size = tab_size; } self.recalc_after_content_swap(); Ok(()) } fn read_file_as_utf8( &mut self, file: &mut File, buf: &mut [MaybeUninit<u8>; 4 * KIBI], first_chunk_len: usize, done: bool, ) -> apperr::Result<()> { { let mut first_chunk = unsafe { buf[..first_chunk_len].assume_init_ref() }; if first_chunk.starts_with(b"\xEF\xBB\xBF") { first_chunk = &first_chunk[3..]; self.encoding = "UTF-8 BOM"; } self.buffer.replace(0..0, first_chunk); } if done { return Ok(()); } // If we don't have file metadata, the input may be a pipe or a socket. // Every read will have the same size until we hit the end. let mut chunk_size = 128 * KIBI; let mut extra_chunk_size = 128 * KIBI; if let Ok(m) = file.metadata() { // Usually the next read of size `chunk_size` will read the entire file, // but if the size has changed for some reason, then `extra_chunk_size` // should be large enough to read the rest of the file. // 4KiB is not too large and not too slow. let len = m.len() as usize; chunk_size = len.saturating_sub(first_chunk_len); extra_chunk_size = 4 * KIBI; } loop { let gap = self.buffer.allocate_gap(self.text_length(), chunk_size, 0); if gap.is_empty() { break; } let read = file.read(gap)?; if read == 0 { break; } self.buffer.commit_gap(read); chunk_size = extra_chunk_size; } Ok(()) } fn read_file_with_icu( &mut self, file: &mut File, buf: &mut [MaybeUninit<u8>; 4 * KIBI], first_chunk_len: usize, mut done: bool, ) -> apperr::Result<()> { let scratch = scratch_arena(None); let pivot_buffer = scratch.alloc_uninit_slice(4 * KIBI); let mut c = icu::Converter::new(pivot_buffer, self.encoding, "UTF-8")?; let mut first_chunk = unsafe { buf[..first_chunk_len].assume_init_ref() }; while !first_chunk.is_empty() { let off = self.text_length(); let gap = self.buffer.allocate_gap(off, 8 * KIBI, 0); let (input_advance, mut output_advance) = c.convert(first_chunk, slice_as_uninit_mut(gap))?; // Remove the BOM from the file, if this is the first chunk. // Our caller ensures to only call us once the BOM has been identified, // which means that if there's a BOM it must be wholly contained in this chunk. if off == 0 { let written = &mut gap[..output_advance]; if written.starts_with(b"\xEF\xBB\xBF") { written.copy_within(3.., 0); output_advance -= 3; } } self.buffer.commit_gap(output_advance); first_chunk = &first_chunk[input_advance..]; } let mut buf_len = 0; loop { if !done { let read = file_read_uninit(file, &mut buf[buf_len..])?; buf_len += read; done = read == 0; } let gap = self.buffer.allocate_gap(self.text_length(), 8 * KIBI, 0); if gap.is_empty() { break; } let read = unsafe { buf[..buf_len].assume_init_ref() }; let (input_advance, output_advance) = c.convert(read, slice_as_uninit_mut(gap))?; self.buffer.commit_gap(output_advance); let flush = done && buf_len == 0; buf_len -= input_advance; buf.copy_within(input_advance.., 0); if flush { break; } } Ok(()) } /// Writes the text buffer contents to a file, handling BOM and encoding. pub fn write_file(&mut self, file: &mut File) -> apperr::Result<()> { let mut offset = 0; if self.encoding.starts_with("UTF-8") { if self.encoding == "UTF-8 BOM" { file.write_all(b"\xEF\xBB\xBF")?; } loop { let chunk = self.read_forward(offset); if chunk.is_empty() { break; } file.write_all(chunk)?; offset += chunk.len(); } } else { self.write_file_with_icu(file)?; } self.mark_as_clean(); Ok(()) } fn write_file_with_icu(&mut self, file: &mut File) -> apperr::Result<()> { let scratch = scratch_arena(None); let pivot_buffer = scratch.alloc_uninit_slice(4 * KIBI); let buf = scratch.alloc_uninit_slice(4 * KIBI); let mut c = icu::Converter::new(pivot_buffer, "UTF-8", self.encoding)?; let mut offset = 0; // Write the BOM for the encodings we know need it. if self.encoding.starts_with("UTF-16") || self.encoding.starts_with("UTF-32") || self.encoding == "GB18030" { let (_, output_advance) = c.convert(b"\xEF\xBB\xBF", buf)?; let chunk = unsafe { buf[..output_advance].assume_init_ref() }; file.write_all(chunk)?; } loop { let chunk = self.read_forward(offset); if chunk.is_empty() { break; } let (input_advance, output_advance) = c.convert(chunk, buf)?; let chunk = unsafe { buf[..output_advance].assume_init_ref() }; file.write_all(chunk)?; offset += input_advance; } Ok(()) } /// Returns the current selection. pub fn has_selection(&self) -> bool { self.selection.is_some() } fn set_selection(&mut self, selection: Option<TextBufferSelection>) -> u32 { self.selection = selection.filter(|s| s.beg != s.end); self.selection_generation = self.selection_generation.wrapping_add(1); self.selection_generation } /// Moves the cursor by `offset` and updates the selection to contain it. pub fn selection_update_offset(&mut self, offset: usize) { self.set_cursor_for_selection(self.cursor_move_to_offset_internal(self.cursor, offset)); } /// Moves the cursor to `visual_pos` and updates the selection to contain it. pub fn selection_update_visual(&mut self, visual_pos: Point) { self.set_cursor_for_selection(self.cursor_move_to_visual_internal(self.cursor, visual_pos)); } /// Moves the cursor to `logical_pos` and updates the selection to contain it. pub fn selection_update_logical(&mut self, logical_pos: Point) { self.set_cursor_for_selection( self.cursor_move_to_logical_internal(self.cursor, logical_pos), ); } /// Moves the cursor by `delta` and updates the selection to contain it. pub fn selection_update_delta(&mut self, granularity: CursorMovement, delta: CoordType) { self.set_cursor_for_selection(self.cursor_move_delta_internal( self.cursor, granularity, delta, )); } /// Select the current word. pub fn select_word(&mut self) { let Range { start, end } = navigation::word_select(&self.buffer, self.cursor.offset); let beg = self.cursor_move_to_offset_internal(self.cursor, start); let end = self.cursor_move_to_offset_internal(beg, end); unsafe { self.set_cursor(end) }; self.set_selection(Some(TextBufferSelection { beg: beg.logical_pos, end: end.logical_pos, })); } /// Select the current line. pub fn select_line(&mut self) { let beg = self.cursor_move_to_logical_internal( self.cursor, Point { x: 0, y: self.cursor.logical_pos.y }, ); let end = self .cursor_move_to_logical_internal(beg, Point { x: 0, y: self.cursor.logical_pos.y + 1 }); unsafe { self.set_cursor(end) }; self.set_selection(Some(TextBufferSelection { beg: beg.logical_pos, end: end.logical_pos, })); } /// Select the entire document. pub fn select_all(&mut self) { let beg = Default::default(); let end = self.cursor_move_to_logical_internal(beg, Point::MAX); unsafe { self.set_cursor(end) }; self.set_selection(Some(TextBufferSelection { beg: beg.logical_pos, end: end.logical_pos, })); } /// Starts a new selection, if there's none already. pub fn start_selection(&mut self) { if self.selection.is_none() { self.set_selection(Some(TextBufferSelection { beg: self.cursor.logical_pos, end: self.cursor.logical_pos, })); } } /// Destroy the current selection. pub fn clear_selection(&mut self) -> bool { let had_selection = self.selection.is_some(); self.set_selection(None); had_selection } /// Find the next occurrence of the given `pattern` and select it. pub fn find_and_select(&mut self, pattern: &str, options: SearchOptions) -> apperr::Result<()> { if let Some(search) = &mut self.search { let search = search.get_mut(); // When the search input changes we must reset the search. if search.pattern != pattern || search.options != options { self.search = None; } // When transitioning from some search to no search, we must clear the selection. if pattern.is_empty() && let Some(TextBufferSelection { beg, .. }) = self.selection { self.cursor_move_to_logical(beg); } } if pattern.is_empty() { return Ok(()); } let search = match &self.search { Some(search) => unsafe { &mut *search.get() }, None => { let search = self.find_construct_search(pattern, options)?; self.search = Some(UnsafeCell::new(search)); unsafe { &mut *self.search.as_ref().unwrap().get() } } }; // If we previously searched through the entire document and found 0 matches, // then we can avoid searching again. if search.no_matches { return Ok(()); } // If the user moved the cursor since the last search, but the needle remained the same, // we still need to move the start of the search to the new cursor position. let next_search_offset = match self.selection { Some(TextBufferSelection { beg, end }) => { if self.selection_generation == search.selection_generation { search.next_search_offset } else { self.cursor_move_to_logical_internal(self.cursor, beg.min(end)).offset } } _ => self.cursor.offset, }; self.find_select_next(search, next_search_offset, true); Ok(()) } /// Find the next occurrence of the given `pattern` and replace it with `replacement`. pub fn find_and_replace( &mut self, pattern: &str, options: SearchOptions, replacement: &str, ) -> apperr::Result<()> { // Editors traditionally replace the previous search hit, not the next possible one. if let (Some(search), Some(..)) = (&mut self.search, &self.selection) { let search = search.get_mut(); if search.selection_generation == self.selection_generation { self.write(replacement.as_bytes(), true); } } self.find_and_select(pattern, options) } /// Find all occurrences of the given `pattern` and replace them with `replacement`. pub fn find_and_replace_all( &mut self, pattern: &str, options: SearchOptions, replacement: &str, ) -> apperr::Result<()> { let replacement = replacement.as_bytes(); let mut search = self.find_construct_search(pattern, options)?; let mut offset = 0; loop { self.find_select_next(&mut search, offset, false); if !self.has_selection() { break; } self.write(replacement, true); offset = self.cursor.offset; } Ok(()) } fn find_construct_search( &self, pattern: &str, options: SearchOptions, ) -> apperr::Result<ActiveSearch> { if pattern.is_empty() { return Err(apperr::Error::Icu(1)); // U_ILLEGAL_ARGUMENT_ERROR } let sanitized_pattern = if options.whole_word && options.use_regex { Cow::Owned(format!(r"\b(?:{pattern})\b")) } else if options.whole_word { let mut p = String::with_capacity(pattern.len() + 16); p.push_str(r"\b"); // Escape regex special characters. let b = unsafe { p.as_mut_vec() }; for &byte in pattern.as_bytes() { match byte { b'*' | b'?' | b'+' | b'[' | b'(' | b')' | b'{' | b'}' | b'^' | b'$' | b'|' | b'\\' | b'.' => { b.push(b'\\'); b.push(byte); } _ => b.push(byte), } } p.push_str(r"\b"); Cow::Owned(p) } else { Cow::Borrowed(pattern) }; let mut flags = icu::Regex::MULTILINE; if !options.match_case { flags |= icu::Regex::CASE_INSENSITIVE; } if !options.use_regex && !options.whole_word { flags |= icu::Regex::LITERAL; } // Move the start of the search to the start of the selection, // or otherwise to the current cursor position. let text = unsafe { icu::Text::new(self)? }; let regex = unsafe { icu::Regex::new(&sanitized_pattern, flags, &text)? }; Ok(ActiveSearch { pattern: pattern.to_string(), options, text, regex, buffer_generation: self.buffer.generation(), selection_generation: 0, next_search_offset: 0, no_matches: false, }) } fn find_select_next(&mut self, search: &mut ActiveSearch, offset: usize, wrap: bool) { if search.buffer_generation != self.buffer.generation() { unsafe { search.regex.set_text(&mut search.text, offset) }; search.buffer_generation = self.buffer.generation(); search.next_search_offset = offset; } else if search.next_search_offset != offset { search.next_search_offset = offset; search.regex.reset(offset); } let mut hit = search.regex.next(); // If we hit the end of the buffer, and we know that there's something to find, // start the search again from the beginning (= wrap around). if wrap && hit.is_none() && search.next_search_offset != 0 { search.next_search_offset = 0; search.regex.reset(0); hit = search.regex.next(); } search.selection_generation = if let Some(range) = hit { // Now the search offset is no more at the start of the buffer. search.next_search_offset = range.end; let beg = self.cursor_move_to_offset_internal(self.cursor, range.start); let end = self.cursor_move_to_offset_internal(beg, range.end); unsafe { self.set_cursor(end) }; self.make_cursor_visible(); self.set_selection(Some(TextBufferSelection { beg: beg.logical_pos, end: end.logical_pos, })) } else { // Avoid searching through the entire document again if we know there's nothing to find. search.no_matches = true; self.set_selection(None) }; } fn measurement_config(&self) -> MeasurementConfig { MeasurementConfig::new(&self.buffer) .with_word_wrap_column(self.word_wrap_column) .with_tab_size(self.tab_size) } fn goto_line_start(&self, cursor: Cursor, y: CoordType) -> Cursor { let mut result = cursor; let mut seek_to_line_start = true; if y > result.logical_pos.y { while y > result.logical_pos.y { let chunk = self.read_forward(result.offset); if chunk.is_empty() { break; } let (delta, line) = simd::lines_fwd(chunk, 0, result.logical_pos.y, y); result.offset += delta; result.logical_pos.y = line; } // If we're at the end of the buffer, we could either be there because the last // character in the buffer is genuinely a newline, or because the buffer ends in a // line of text without trailing newline. The only way to make sure is to seek // backwards to the line start again. But otherwise we can skip that. seek_to_line_start = result.offset == self.text_length() && result.offset != cursor.offset; } if seek_to_line_start { loop { let chunk = self.read_backward(result.offset); if chunk.is_empty() { break; } let (delta, line) = simd::lines_bwd(chunk, chunk.len(), result.logical_pos.y, y); result.offset -= chunk.len() - delta; result.logical_pos.y = line; if delta > 0 { break; } } } if result.offset == cursor.offset { return result; } result.logical_pos.x = 0; result.visual_pos.x = 0; result.visual_pos.y = result.logical_pos.y; result.column = 0; result.wrap_opp = false; if self.word_wrap_column > 0 { let upward = result.offset < cursor.offset; let (top, bottom) = if upward { (result, cursor) } else { (cursor, result) }; let mut bottom_remeasured = self.measurement_config().with_cursor(top).goto_logical(bottom.logical_pos); // The second problem is that visual positions can be ambiguous. A single logical position // can map to two visual positions: One at the end of the preceding line in front of // a word wrap, and another at the start of the next line after the same word wrap. // // This, however, only applies if we go upwards, because only then `bottom ≅ cursor`, // and thus only then this `bottom` is ambiguous. Otherwise, `bottom ≅ result` // and `result` is at a line start which is never ambiguous. if upward { let a = bottom_remeasured.visual_pos.x; let b = bottom.visual_pos.x; bottom_remeasured.visual_pos.y = bottom_remeasured.visual_pos.y + (a != 0 && b == 0) as CoordType - (a == 0 && b != 0) as CoordType; } let mut delta = bottom_remeasured.visual_pos.y - top.visual_pos.y; if upward { delta = -delta; } result.visual_pos.y = cursor.visual_pos.y + delta; } result } fn cursor_move_to_offset_internal(&self, mut cursor: Cursor, offset: usize) -> Cursor { if offset == cursor.offset { return cursor; } // goto_line_start() is fast for seeking across lines _if_ line wrapping is disabled. // For backward seeking we have to use it either way, so we're covered there. // This implements the forward seeking portion, if it's approx. worth doing so. if self.word_wrap_column <= 0 && offset.saturating_sub(cursor.offset) > 1024 { // Replacing this with a more optimal, direct memchr() loop appears // to improve performance only marginally by another 2% or so. // Still, it's kind of "meh" looking at how poorly this is implemented... loop { let next = self.goto_line_start(cursor, cursor.logical_pos.y + 1); // Stop when we either ran past the target offset, // or when we hit the end of the buffer and `goto_line_start` backtracked to the line start. if next.offset > offset || next.offset <= cursor.offset { break; } cursor = next; } } while offset < cursor.offset { cursor = self.goto_line_start(cursor, cursor.logical_pos.y - 1); } self.measurement_config().with_cursor(cursor).goto_offset(offset) } fn cursor_move_to_logical_internal(&self, mut cursor: Cursor, pos: Point) -> Cursor { let pos = Point { x: pos.x.max(0), y: pos.y.max(0) }; if pos == cursor.logical_pos { return cursor; } // goto_line_start() is the fastest way for seeking across lines. As such we always // use it if the requested `.y` position is different. We still need to use it if the // `.x` position is smaller, but only because `goto_logical()` cannot seek backwards. if pos.y != cursor.logical_pos.y || pos.x < cursor.logical_pos.x { cursor = self.goto_line_start(cursor, pos.y); } self.measurement_config().with_cursor(cursor).goto_logical(pos) } fn cursor_move_to_visual_internal(&self, mut cursor: Cursor, pos: Point) -> Cursor { let pos = Point { x: pos.x.max(0), y: pos.y.max(0) }; if pos == cursor.visual_pos { return cursor; } if self.word_wrap_column <= 0 { // Identical to the fast-pass in `cursor_move_to_logical_internal()`. if pos.y != cursor.visual_pos.y || pos.x < cursor.visual_pos.x { cursor = self.goto_line_start(cursor, pos.y); } } else { // `goto_visual()` can only seek forward, so we need to seek backward here if needed. // NOTE that this intentionally doesn't use the `Eq` trait of `Point`, because if // `pos.y == cursor.visual_pos.y` we don't need to go to `cursor.logical_pos.y - 1`. while pos.y < cursor.visual_pos.y { cursor = self.goto_line_start(cursor, cursor.logical_pos.y - 1); } if pos.y == cursor.visual_pos.y && pos.x < cursor.visual_pos.x { cursor = self.goto_line_start(cursor, cursor.logical_pos.y); } } self.measurement_config().with_cursor(cursor).goto_visual(pos) } fn cursor_move_delta_internal( &self, mut cursor: Cursor, granularity: CursorMovement, mut delta: CoordType, ) -> Cursor { if delta == 0 { return cursor; } let sign = if delta > 0 { 1 } else { -1 }; match granularity { CursorMovement::Grapheme => { let start_x = if delta > 0 { 0 } else { CoordType::MAX }; loop { let target_x = cursor.logical_pos.x + delta; cursor = self.cursor_move_to_logical_internal( cursor, Point { x: target_x, y: cursor.logical_pos.y }, ); // We can stop if we ran out of remaining delta // (or perhaps ran past the goal; in either case the sign would've changed), // or if we hit the beginning or end of the buffer. delta = target_x - cursor.logical_pos.x; if delta.signum() != sign || (delta < 0 && cursor.offset == 0) || (delta > 0 && cursor.offset >= self.text_length()) { break; } cursor = self.cursor_move_to_logical_internal( cursor, Point { x: start_x, y: cursor.logical_pos.y + sign }, ); // We crossed a newline which counts for 1 grapheme cluster. // So, we also need to run the same check again. delta -= sign; if delta.signum() != sign || cursor.offset == 0 || cursor.offset >= self.text_length() { break; } } } CursorMovement::Word => { let doc = &self.buffer as &dyn ReadableDocument; let mut offset = self.cursor.offset; while delta != 0 { if delta < 0 { offset = navigation::word_backward(doc, offset); } else { offset = navigation::word_forward(doc, offset); } delta -= sign; } cursor = self.cursor_move_to_offset_internal(cursor, offset); } } cursor } /// Moves the cursor to the given offset. pub fn cursor_move_to_offset(&mut self, offset: usize) { unsafe { self.set_cursor(self.cursor_move_to_offset_internal(self.cursor, offset)) } } /// Moves the cursor to the given logical position. pub fn cursor_move_to_logical(&mut self, pos: Point) { unsafe { self.set_cursor(self.cursor_move_to_logical_internal(self.cursor, pos)) } } /// Moves the cursor to the given visual position. pub fn cursor_move_to_visual(&mut self, pos: Point) { unsafe { self.set_cursor(self.cursor_move_to_visual_internal(self.cursor, pos)) } } /// Moves the cursor by the given delta. pub fn cursor_move_delta(&mut self, granularity: CursorMovement, delta: CoordType) { unsafe { self.set_cursor(self.cursor_move_delta_internal(self.cursor, granularity, delta)) } } /// Sets the cursor to the given position, and clears the selection. /// /// # Safety /// /// This function performs no checks that the cursor is valid. "Valid" in this case means /// that the TextBuffer has not been modified since you received the cursor from this class. pub unsafe fn set_cursor(&mut self, cursor: Cursor) { self.set_cursor_internal(cursor); self.last_history_type = HistoryType::Other; self.set_selection(None); } fn set_cursor_for_selection(&mut self, cursor: Cursor) { let beg = match self.selection { Some(TextBufferSelection { beg, .. }) => beg, None => self.cursor.logical_pos, }; self.set_cursor_internal(cursor); self.last_history_type = HistoryType::Other; let end = self.cursor.logical_pos; self.set_selection(if beg == end { None } else { Some(TextBufferSelection { beg, end }) }); } fn set_cursor_internal(&mut self, cursor: Cursor) { debug_assert!( cursor.offset <= self.text_length() && cursor.logical_pos.x >= 0 && cursor.logical_pos.y >= 0 && cursor.logical_pos.y <= self.stats.logical_lines && cursor.visual_pos.x >= 0 && (self.word_wrap_column <= 0 || cursor.visual_pos.x <= self.word_wrap_column) && cursor.visual_pos.y >= 0 && cursor.visual_pos.y <= self.stats.visual_lines ); self.cursor = cursor; } /// Extracts a rectangular region of the text buffer and writes it to the framebuffer. /// The `destination` rect is framebuffer coordinates. The extracted region within this /// text buffer has the given `origin` and the same size as the `destination` rect. pub fn render( &mut self, origin: Point, destination: Rect, focused: bool, fb: &mut Framebuffer, ) -> Option<RenderResult> { if destination.is_empty() { return None; } let scratch = scratch_arena(None); let width = destination.width(); let height = destination.height(); let line_number_width = self.margin_width.max(3) as usize - 3; let text_width = width - self.margin_width; let mut visualizer_buf = [0xE2, 0x90, 0x80]; // U+2400 in UTF8 let mut line = ArenaString::new_in(&scratch); let mut visual_pos_x_max = 0; // Pick the cursor closer to the `origin.y`. let mut cursor = { let a = self.cursor; let b = self.cursor_for_rendering.unwrap_or_default(); let da = (a.visual_pos.y - origin.y).abs(); let db = (b.visual_pos.y - origin.y).abs(); if da < db { a } else { b } }; let [selection_beg, selection_end] = match self.selection { None => [Point::MIN, Point::MIN], Some(TextBufferSelection { beg, end }) => minmax(beg, end), }; line.reserve(width as usize * 2); for y in 0..height { line.clear(); let visual_line = origin.y + y; let mut cursor_beg = self.cursor_move_to_visual_internal(cursor, Point { x: origin.x, y: visual_line }); let cursor_end = self.cursor_move_to_visual_internal( cursor_beg, Point { x: origin.x + text_width, y: visual_line }, ); // Accelerate the next render pass by remembering where we started off. if y == 0 { self.cursor_for_rendering = Some(cursor_beg); } if line_number_width != 0 { if visual_line >= self.stats.visual_lines { // Past the end of the buffer? Place " | " in the margin. // Since we know that we won't see line numbers greater than i64::MAX (9223372036854775807) // any time soon, we can use a static string as the template (`MARGIN`) and slice it, // because `line_number_width` can't possibly be larger than 19. let off = 19 - line_number_width; unsafe { std::hint::assert_unchecked(off < MARGIN_TEMPLATE.len()) }; line.push_str(&MARGIN_TEMPLATE[off..]); } else if self.word_wrap_column <= 0 || cursor_beg.logical_pos.x == 0 { // Regular line? Place "123 | " in the margin. _ = write!(line, "{:1$} │ ", cursor_beg.logical_pos.y + 1, line_number_width); } else { // Wrapped line? Place " ... | " in the margin. let number_width = (cursor_beg.logical_pos.y + 1).ilog10() as usize + 1; _ = write!( line, "{0:1$}{0:∙<2$} │ ", "", line_number_width - number_width, number_width ); // Blending in the background color will "dim" the indicator dots. let left = destination.left; let top = destination.top + y; fb.blend_fg( Rect { left, top, right: left + line_number_width as CoordType, bottom: top + 1, }, fb.indexed_alpha(IndexedColor::Background, 1, 2), ); } } // Nothing to do if the entire line is empty. if cursor_beg.offset != cursor_end.offset { // If we couldn't reach the left edge, we may have stopped short due to a wide glyph. // In that case we'll try to find the next character and then compute by how many // columns it overlaps the left edge (can be anything between 1 and 7). if cursor_beg.visual_pos.x < origin.x { let cursor_next = self.cursor_move_to_logical_internal( cursor_beg, Point { x: cursor_beg.logical_pos.x + 1, y: cursor_beg.logical_pos.y }, ); if cursor_next.visual_pos.x > origin.x { let overlap = cursor_next.visual_pos.x - origin.x; debug_assert!((1..=7).contains(&overlap)); line.push_str(&TAB_WHITESPACE[..overlap as usize]); cursor_beg = cursor_next; } } fn find_control_char(text: &[u8], mut offset: usize) -> usize { while offset < text.len() && (text[offset] >= 0x20 && text[offset] != 0x7f) { offset += 1; } offset } let mut global_off = cursor_beg.offset; let mut cursor_tab = cursor_beg; let mut cursor_visualizer = cursor_beg; while global_off < cursor_end.offset { let chunk = self.read_forward(global_off); let chunk = &chunk[..chunk.len().min(cursor_end.offset - global_off)]; let mut chunk_off = 0; while chunk_off < chunk.len() { let beg = chunk_off; chunk_off = find_control_char(chunk, beg); for chunk in chunk[beg..chunk_off].utf8_chunks() { if !chunk.valid().is_empty() { line.push_str(chunk.valid()); } if !chunk.invalid().is_empty() { line.push('\u{FFFD}'); } } while chunk_off < chunk.len() && (chunk[chunk_off] < 0x20 || chunk[chunk_off] == 0x7f) { let ch = chunk[chunk_off]; chunk_off += 1; if ch == b'\t' { cursor_tab = self.cursor_move_to_offset_internal( cursor_tab, global_off + chunk_off - 1, ); let tab_size = self.tab_size - (cursor_tab.column % self.tab_size); line.push_str(&TAB_WHITESPACE[..tab_size as usize]); // Since we know that we just aligned ourselves to the next tab stop, // we can trivially process any successive tabs. while chunk_off < chunk.len() && chunk[chunk_off] == b'\t' { line.push_str(&TAB_WHITESPACE[..self.tab_size as usize]); chunk_off += 1; } continue; } visualizer_buf[2] = if ch == 0x7F { 0xA1 // U+2421 } else { 0x80 | ch // 0x00..=0x1F => U+2400..=U+241F }; // Our manually constructed UTF8 is never going to be invalid. Trust. line.push_str(unsafe { str::from_utf8_unchecked(&visualizer_buf) }); cursor_visualizer = self.cursor_move_to_offset_internal( cursor_visualizer, global_off + chunk_off - 1, ); let visualizer_rect = { let left = destination.left + self.margin_width + cursor_visualizer.visual_pos.x - origin.x; let top = destination.top + cursor_visualizer.visual_pos.y - origin.y; Rect { left, top, right: left + 1, bottom: top + 1 } }; let bg = fb.indexed(IndexedColor::Yellow); let fg = fb.contrasted(bg); fb.blend_bg(visualizer_rect, bg); fb.blend_fg(visualizer_rect, fg); } } global_off += chunk.len(); } visual_pos_x_max = visual_pos_x_max.max(cursor_end.visual_pos.x); } fb.replace_text(destination.top + y, destination.left, destination.right, &line); // Draw the selection on this line, if any. // FYI: `cursor_beg.visual_pos.y == visual_line` is necessary as the `visual_line` // may be past the end of the document, and so it may not receive a highlight. if cursor_beg.visual_pos.y == visual_line && selection_beg <= cursor_end.logical_pos && selection_end >= cursor_beg.logical_pos { // By default, we assume the entire line is selected. let mut beg = 0; let mut end = COORD_TYPE_SAFE_MAX; let mut cursor = cursor_beg; // The start of the selection is within this line. We need to update selection_beg. if selection_beg <= cursor_end.logical_pos && selection_beg >= cursor_beg.logical_pos { cursor = self.cursor_move_to_logical_internal(cursor, selection_beg); beg = cursor.visual_pos.x; } // The end of the selection is within this line. We need to update selection_end. if selection_end <= cursor_end.logical_pos && selection_end >= cursor_beg.logical_pos { cursor = self.cursor_move_to_logical_internal(cursor, selection_end); end = cursor.visual_pos.x; } beg = beg.max(origin.x); end = end.min(origin.x + text_width); let left = destination.left + self.margin_width - origin.x; let top = destination.top + y; let rect = Rect { left: left + beg, top, right: left + end, bottom: top + 1 }; let mut bg = oklab_blend( fb.indexed(IndexedColor::Foreground), fb.indexed_alpha(IndexedColor::BrightBlue, 1, 2), ); if !focused { bg = oklab_blend(bg, fb.indexed_alpha(IndexedColor::Background, 1, 2)) }; let fg = fb.contrasted(bg); fb.blend_bg(rect, bg); fb.blend_fg(rect, fg); } cursor = cursor_end; } // Colorize the margin that we wrote above. if self.margin_width > 0 { let margin = Rect { left: destination.left, top: destination.top, right: destination.left + self.margin_width, bottom: destination.bottom, }; fb.blend_fg(margin, 0x7f3f3f3f); } if self.ruler > 0 { let left = destination.left + self.margin_width + (self.ruler - origin.x).max(0); let right = destination.right; if left < right { fb.blend_bg( Rect { left, top: destination.top, right, bottom: destination.bottom }, fb.indexed_alpha(IndexedColor::BrightRed, 1, 4), ); } } if focused { let mut x = self.cursor.visual_pos.x; let mut y = self.cursor.visual_pos.y; if self.word_wrap_column > 0 && x >= self.word_wrap_column { // The line the cursor is on wraps exactly on the word wrap column which // means the cursor is invisible. We need to move it to the next line. x = 0; y += 1; } // Move the cursor into screen space. x += destination.left - origin.x + self.margin_width; y += destination.top - origin.y; let cursor = Point { x, y }; let text = Rect { left: destination.left + self.margin_width, top: destination.top, right: destination.right, bottom: destination.bottom, }; if text.contains(cursor) { fb.set_cursor(cursor, self.overtype); if self.line_highlight_enabled && selection_beg >= selection_end { fb.blend_bg( Rect { left: destination.left, top: cursor.y, right: destination.right, bottom: cursor.y + 1, }, 0x50282828, ); } } } Some(RenderResult { visual_pos_x_max }) } /// Inserts `text` at the current cursor position. /// /// If there's a current selection, it will be replaced. /// The selection is cleared after the call. pub fn write(&mut self, text: &[u8], raw: bool) { if text.is_empty() { return; } if let Some((beg, end)) = self.selection_range_internal(false) { self.edit_begin(HistoryType::Write, beg); self.edit_delete(end); self.set_selection(None); } if self.active_edit_depth <= 0 { self.edit_begin(HistoryType::Write, self.cursor); } let mut offset = 0; let scratch = scratch_arena(None); let mut newline_buffer = ArenaString::new_in(&scratch); loop { // Can't use `unicode::newlines_forward` because bracketed paste uses CR instead of LF/CRLF. let offset_next = memchr2(b'\r', b'\n', text, offset); let line = &text[offset..offset_next]; let column_before = self.cursor.logical_pos.x; // Write the contents of the line into the buffer. let mut line_off = 0; while line_off < line.len() { // Split the line into chunks of non-tabs and tabs. let mut plain = line; if !raw && !self.indent_with_tabs { let end = memchr2(b'\t', b'\t', line, line_off); plain = &line[line_off..end]; } // Non-tabs are written as-is, because the outer loop already handles newline translation. self.edit_write(plain); line_off += plain.len(); // Now replace tabs with spaces. while line_off < line.len() && line[line_off] == b'\t' { let spaces = self.tab_size - (self.cursor.column % self.tab_size); let spaces = &TAB_WHITESPACE.as_bytes()[..spaces as usize]; self.edit_write(spaces); line_off += 1; } } if !raw && self.overtype { let delete = self.cursor.logical_pos.x - column_before; let end = self.cursor_move_to_logical_internal( self.cursor, Point { x: self.cursor.logical_pos.x + delete, y: self.cursor.logical_pos.y }, ); self.edit_delete(end); } offset += line.len(); if offset >= text.len() { break; } // First, write the newline. newline_buffer.clear(); newline_buffer.push_str(if self.newlines_are_crlf { "\r\n" } else { "\n" }); if !raw { // We'll give the next line the same indentation as the previous one. // This block figures out how much that is. We can't reuse that value, // because " a\n a\n" should give the 3rd line a total indentation of 4. // Assuming your terminal has bracketed paste, this won't be a concern though. // (If it doesn't, use a different terminal.) let tab_size = self.tab_size as usize; let line_beg = self.goto_line_start(self.cursor, self.cursor.logical_pos.y); let limit = self.cursor.offset; let mut off = line_beg.offset; let mut newline_indentation = 0usize; 'outer: while off < limit { let chunk = self.read_forward(off); let chunk = &chunk[..chunk.len().min(limit - off)]; for &c in chunk { if c == b' ' { newline_indentation += 1; } else if c == b'\t' { newline_indentation += tab_size - (newline_indentation % tab_size); } else { break 'outer; } } off += chunk.len(); } // If tabs are enabled, add as many tabs as we can. if self.indent_with_tabs { let tab_count = newline_indentation / tab_size; newline_buffer.push_repeat('\t', tab_count); newline_indentation -= tab_count * tab_size; } // If tabs are disabled, or if the indentation wasn't a multiple of the tab size, // add spaces to make up the difference. newline_buffer.push_repeat(' ', newline_indentation); } self.edit_write(newline_buffer.as_bytes()); // Skip one CR/LF/CRLF. if offset >= text.len() { break; } if text[offset] == b'\r' { offset += 1; } if offset >= text.len() { break; } if text[offset] == b'\n' { offset += 1; } if offset >= text.len() { break; } } // POSIX mandates that all valid lines end in a newline. // This isn't all that common on Windows and so we have // `self.final_newline` to control this. // // In order to not annoy people with this, we only add a // newline if you just edited the very end of the buffer. if self.insert_final_newline && self.cursor.offset > 0 && self.cursor.offset == self.text_length() && self.cursor.logical_pos.x > 0 { let cursor = self.cursor; self.edit_write(if self.newlines_are_crlf { b"\r\n" } else { b"\n" }); self.set_cursor_internal(cursor); } self.edit_end(); } /// Deletes 1 grapheme cluster from the buffer. /// `cursor_movements` is expected to be -1 for backspace and 1 for delete. /// If there's a current selection, it will be deleted and `cursor_movements` ignored. /// The selection is cleared after the call. /// Deletes characters from the buffer based on a delta from the cursor. pub fn delete(&mut self, granularity: CursorMovement, delta: CoordType) { if delta == 0 { return; } let mut beg; let mut end; if let Some(r) = self.selection_range_internal(false) { (beg, end) = r; } else { if (delta < 0 && self.cursor.offset == 0) || (delta > 0 && self.cursor.offset >= self.text_length()) { // Nothing to delete. return; } beg = self.cursor; end = self.cursor_move_delta_internal(beg, granularity, delta); if beg.offset == end.offset { return; } if beg.offset > end.offset { mem::swap(&mut beg, &mut end); } } self.edit_begin(HistoryType::Delete, beg); self.edit_delete(end); self.edit_end(); self.set_selection(None); } /// Returns the logical position of the first character on this line. /// Return `.x == 0` if there are no non-whitespace characters. pub fn indent_end_logical_pos(&self) -> Point { let cursor = self.goto_line_start(self.cursor, self.cursor.logical_pos.y); let mut chars = 0; let mut offset = cursor.offset; 'outer: loop { let chunk = self.read_forward(offset); if chunk.is_empty() { break; } for &c in chunk { if c == b'\n' || c == b'\r' || (c != b' ' && c != b'\t') { break 'outer; } chars += 1; } offset += chunk.len(); } Point { x: chars, y: cursor.logical_pos.y } } /// Unindents the current selection or line. /// /// TODO: This function is ripe for some optimizations: /// * Instead of replacing the entire selection, /// it should unindent each line directly (as if multiple cursors had been used). /// * The cursor movement at the end is rather costly, but at least without word wrap /// it should be possible to calculate it directly from the removed amount. pub fn unindent(&mut self) { let mut selection_beg = self.cursor.logical_pos; let mut selection_end = selection_beg; if let Some(TextBufferSelection { beg, end }) = self.selection { selection_beg = beg; selection_end = end; } let [beg, end] = minmax(selection_beg, selection_end); let beg = self.cursor_move_to_logical_internal(self.cursor, Point { x: 0, y: beg.y }); let end = self.cursor_move_to_logical_internal(beg, Point { x: CoordType::MAX, y: end.y }); let mut replacement = Vec::new(); self.buffer.extract_raw(beg.offset..end.offset, &mut replacement, 0); let initial_len = replacement.len(); let mut offset = 0; let mut y = beg.logical_pos.y; loop { if offset >= replacement.len() { break; } let mut remove = 0; if replacement[offset] == b'\t' { remove = 1; } else { while remove < self.tab_size as usize && offset + remove < replacement.len() && replacement[offset + remove] == b' ' { remove += 1; } } if remove > 0 { replacement.drain(offset..offset + remove); } if y == selection_beg.y { selection_beg.x -= remove as CoordType; } if y == selection_end.y { selection_end.x -= remove as CoordType; } (offset, y) = simd::lines_fwd(&replacement, offset, y, y + 1); } if replacement.len() == initial_len { // Nothing to do. return; } self.edit_begin(HistoryType::Other, beg); self.edit_delete(end); self.edit_write(&replacement); self.edit_end(); if let Some(TextBufferSelection { beg, end }) = &mut self.selection { *beg = selection_beg; *end = selection_end; } self.set_cursor_internal(self.cursor_move_to_logical_internal(self.cursor, selection_end)); } /// Extracts the contents of the current selection. /// May optionally delete it, if requested. This is meant to be used for Ctrl+X. pub fn extract_selection(&mut self, delete: bool) -> Vec<u8> { let Some((beg, end)) = self.selection_range_internal(true) else { return Vec::new(); }; let mut out = Vec::new(); self.buffer.extract_raw(beg.offset..end.offset, &mut out, 0); if delete && !out.is_empty() { self.edit_begin(HistoryType::Delete, beg); self.edit_delete(end); self.edit_end(); self.set_selection(None); } out } /// Extracts the contents of the current selection the user made. /// This differs from [`TextBuffer::extract_selection()`] in that /// it does nothing if the selection was made by searching. pub fn extract_user_selection(&mut self, delete: bool) -> Option<Vec<u8>> { if !self.has_selection() { return None; } if let Some(search) = &self.search { let search = unsafe { &*search.get() }; if search.selection_generation == self.selection_generation { return None; } } Some(self.extract_selection(delete)) } /// Returns the current selection anchors, or `None` if there /// is no selection. The returned logical positions are sorted. pub fn selection_range(&self) -> Option<(Cursor, Cursor)> { self.selection_range_internal(false) } /// Returns the current selection anchors. /// /// If there's no selection and `line_fallback` is `true`, /// the start/end of the current line are returned. /// This is meant to be used for Ctrl+C / Ctrl+X. fn selection_range_internal(&self, line_fallback: bool) -> Option<(Cursor, Cursor)> { let [beg, end] = match self.selection { None if !line_fallback => return None, None => [ Point { x: 0, y: self.cursor.logical_pos.y }, Point { x: 0, y: self.cursor.logical_pos.y + 1 }, ], Some(TextBufferSelection { beg, end }) => minmax(beg, end), }; let beg = self.cursor_move_to_logical_internal(self.cursor, beg); let end = self.cursor_move_to_logical_internal(beg, end); if beg.offset < end.offset { Some((beg, end)) } else { None } } /// Starts a new edit operation. /// This is used for tracking the undo/redo history. fn edit_begin(&mut self, history_type: HistoryType, cursor: Cursor) { self.active_edit_depth += 1; if self.active_edit_depth > 1 { return; } let cursor_before = self.cursor; self.set_cursor_internal(cursor); // If both the last and this are a Write/Delete operation, we skip allocating a new undo history item. if history_type != self.last_history_type || !matches!(history_type, HistoryType::Write | HistoryType::Delete) { self.redo_stack.clear(); while self.undo_stack.len() > 1000 { self.undo_stack.pop_front(); } self.last_history_type = history_type; self.undo_stack.push_back(SemiRefCell::new(HistoryEntry { cursor_before: cursor_before.logical_pos, selection_before: self.selection, stats_before: self.stats, generation_before: self.buffer.generation(), cursor: cursor.logical_pos, deleted: Vec::new(), added: Vec::new(), })); } self.active_edit_off = cursor.offset; // If word-wrap is enabled, the visual layout of all logical lines affected by the write // may have changed. This includes even text before the insertion point up to the line // start, because this write may have joined with a word before the initial cursor. // See other uses of `word_wrap_cursor_next_line` in this function. if self.word_wrap_column > 0 { let safe_start = self.goto_line_start(cursor, cursor.logical_pos.y); let next_line = self.cursor_move_to_logical_internal( cursor, Point { x: 0, y: cursor.logical_pos.y + 1 }, ); self.active_edit_line_info = Some(ActiveEditLineInfo { safe_start, line_height_in_rows: next_line.visual_pos.y - safe_start.visual_pos.y, distance_next_line_start: next_line.offset - cursor.offset, }); } } /// Writes `text` into the buffer at the current cursor position. /// It records the change in the undo stack. fn edit_write(&mut self, text: &[u8]) { let logical_y_before = self.cursor.logical_pos.y; // Copy the written portion into the undo entry. { let mut undo = self.undo_stack.back_mut().unwrap().borrow_mut(); undo.added.extend_from_slice(text); } // Write! self.buffer.replace(self.active_edit_off..self.active_edit_off, text); // Move self.cursor to the end of the newly written text. Can't use `self.set_cursor_internal`, // because we're still in the progress of recalculating the line stats. self.active_edit_off += text.len(); self.cursor = self.cursor_move_to_offset_internal(self.cursor, self.active_edit_off); self.stats.logical_lines += self.cursor.logical_pos.y - logical_y_before; } /// Deletes the text between the current cursor position and `to`. /// It records the change in the undo stack. fn edit_delete(&mut self, to: Cursor) { debug_assert!(to.offset >= self.active_edit_off); let logical_y_before = self.cursor.logical_pos.y; let off = self.active_edit_off; let mut out_off = usize::MAX; let mut undo = self.undo_stack.back_mut().unwrap().borrow_mut(); if self.cursor.logical_pos < undo.cursor { out_off = 0; // Prepend the deleted portion. undo.cursor = self.cursor.logical_pos; // Note the start of the deleted portion. } // Copy the deleted portion into the undo entry. let deleted = &mut undo.deleted; self.buffer.extract_raw(off..to.offset, deleted, out_off); // Delete the portion from the buffer by enlarging the gap. let count = to.offset - off; self.buffer.allocate_gap(off, 0, count); self.stats.logical_lines += logical_y_before - to.logical_pos.y; } /// Finalizes the current edit operation /// and recalculates the line statistics. fn edit_end(&mut self) { self.active_edit_depth -= 1; assert!(self.active_edit_depth >= 0); if self.active_edit_depth > 0 { return; } #[cfg(debug_assertions)] { let entry = self.undo_stack.back_mut().unwrap().borrow_mut(); debug_assert!(!entry.deleted.is_empty() || !entry.added.is_empty()); } if let Some(info) = self.active_edit_line_info.take() { let deleted_count = self.undo_stack.back_mut().unwrap().borrow_mut().deleted.len(); let target = self.cursor.logical_pos; // From our safe position we can measure the actual visual position of the cursor. self.set_cursor_internal(self.cursor_move_to_logical_internal(info.safe_start, target)); // If content is added at the insertion position, that's not a problem: // We can just remeasure the height of this one line and calculate the delta. // `deleted_count` is 0 in this case. // // The problem is when content is deleted, because it may affect lines // beyond the end of the `next_line`. In that case we have to measure // the entire buffer contents until the end to compute `self.stats.visual_lines`. if deleted_count < info.distance_next_line_start { // Now we can measure how many more visual rows this logical line spans. let next_line = self .cursor_move_to_logical_internal(self.cursor, Point { x: 0, y: target.y + 1 }); let lines_before = info.line_height_in_rows; let lines_after = next_line.visual_pos.y - info.safe_start.visual_pos.y; self.stats.visual_lines += lines_after - lines_before; } else { let end = self.cursor_move_to_logical_internal(self.cursor, Point::MAX); self.stats.visual_lines = end.visual_pos.y + 1; } } else { // If word-wrap is disabled the visual line count always matches the logical one. self.stats.visual_lines = self.stats.logical_lines; } self.search = None; self.cursor_for_rendering = None; } /// Undo the last edit operation. pub fn undo(&mut self) { self.undo_redo(true); } /// Redo the last undo operation. pub fn redo(&mut self) { self.undo_redo(false); } fn undo_redo(&mut self, undo: bool) { // Transfer the last entry from the undo stack to the redo stack or vice versa. { let (from, to) = if undo { (&mut self.undo_stack, &mut self.redo_stack) } else { (&mut self.redo_stack, &mut self.undo_stack) }; let Some(list) = from.cursor_back_mut().remove_current_as_list() else { return; }; to.cursor_back_mut().splice_after(list); } let change = { let to = if undo { &self.redo_stack } else { &self.undo_stack }; to.back().unwrap() }; // Move to the point where the modification took place. let cursor = self.cursor_move_to_logical_internal(self.cursor, change.borrow().cursor); let safe_cursor = if self.word_wrap_column > 0 { // If word-wrap is enabled, we need to move the cursor to the beginning of the line. // This is because the undo/redo operation may have changed the visual position of the cursor. self.goto_line_start(cursor, cursor.logical_pos.y) } else { cursor }; { let buffer_generation = self.buffer.generation(); let mut change = change.borrow_mut(); let change = &mut *change; // Undo: Whatever was deleted is now added and vice versa. mem::swap(&mut change.deleted, &mut change.added); // Delete the inserted portion. self.buffer.allocate_gap(cursor.offset, 0, change.deleted.len()); // Reinsert the deleted portion. { let added = &change.added[..]; let mut beg = 0; let mut offset = cursor.offset; while beg < added.len() { let (end, line) = simd::lines_fwd(added, beg, 0, 1); let has_newline = line != 0; let link = &added[beg..end]; let line = unicode::strip_newline(link); let mut written; { let gap = self.buffer.allocate_gap(offset, line.len() + 2, 0); written = slice_copy_safe(gap, line); if has_newline { if self.newlines_are_crlf && written < gap.len() { gap[written] = b'\r'; written += 1; } if written < gap.len() { gap[written] = b'\n'; written += 1; } } self.buffer.commit_gap(written); } beg = end; offset += written; } } // Restore the previous line statistics. mem::swap(&mut self.stats, &mut change.stats_before); // Restore the previous selection. mem::swap(&mut self.selection, &mut change.selection_before); // Pretend as if the buffer was never modified. self.buffer.set_generation(change.generation_before); change.generation_before = buffer_generation; // Restore the previous cursor. let cursor_before = self.cursor_move_to_logical_internal(safe_cursor, change.cursor_before); change.cursor_before = self.cursor.logical_pos; // Can't use `set_cursor_internal` here, because we haven't updated the line stats yet. self.cursor = cursor_before; if self.undo_stack.is_empty() { self.last_history_type = HistoryType::Other; } } self.cursor_for_rendering = None; } /// For interfacing with ICU. pub(crate) fn read_backward(&self, off: usize) -> &[u8] { self.buffer.read_backward(off) } /// For interfacing with ICU. pub fn read_forward(&self, off: usize) -> &[u8] { self.buffer.read_forward(off) } } pub enum Bom { None, UTF8, UTF16LE, UTF16BE, UTF32LE, UTF32BE, GB18030, } const BOM_MAX_LEN: usize = 4; fn detect_bom(bytes: &[u8]) -> Option<&'static str> { if bytes.len() >= 4 { if bytes.starts_with(b"\xFF\xFE\x00\x00") { return Some("UTF-32LE"); } if bytes.starts_with(b"\x00\x00\xFE\xFF") { return Some("UTF-32BE"); } if bytes.starts_with(b"\x84\x31\x95\x33") { return Some("GB18030"); } } if bytes.len() >= 3 && bytes.starts_with(b"\xEF\xBB\xBF") { return Some("UTF-8"); } if bytes.len() >= 2 { if bytes.starts_with(b"\xFF\xFE") { return Some("UTF-16LE"); } if bytes.starts_with(b"\xFE\xFF") { return Some("UTF-16BE"); } } None }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mixelpixx/microsoft-edit-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server